오늘 풀어본 문제는 백준의 30805번 문제1이다. 문제 풀이에 사용한 언어는 C++ 이다.
이 문제의 내용과 조건은 다음과 같다.
어떤 수열이 다른 수열의 부분 수열이라는 것은 다음을 의미합니다.
또한, 어떤 수열이 다른 수열보다 사전 순으로 나중이라는 것은 다음을 의미합니다.
양의 정수로 이루어진 길이가 $N$ 인 수열 ${A_1,\cdots ,A_N}$ 이 주어집니다. 마찬가지로 양의 정수로 이루어진 길이가 $M$ 인 수열 ${B_1,\cdots ,B_M}$ 이 주어집니다.
수열 $A$ 와 수열 $B$ 가 공통으로 갖는 부분 수열들 중 사전 순으로 가장 나중인 것을 구하세요.
첫 줄에 수열 $A$ 의 길이 $N$ 이 주어집니다. $(1 \le N \le 100)$
둘째 줄에 $N$ 개의 양의 정수 $A_1,A_2,\cdots,A_N$ 이 주어집니다. $(1 \le A_i \le 100)$
셋째 줄에 수열 $B$ 의 길이 $M$ 이 주어집니다. $(1 \le M \le 100)$
넷째 줄에 $M$ 개의 양의 정수 $B_1,B_2,\cdots,B_M$ 이 주어집니다.
$A$ 와 $B$ 의 공통 부분 수열 중 사전 순으로 가장 나중인 수열의 크기 $K$ 를 출력하세요.
$K \ne 0$ 이라면, 다음 줄에 $K$ 개의 수를 공백으로 구분해 출력하세요. $i$ 번째 수는 $A$ 와 $B$ 의 공통 부분 수열 중 사전 순으로 가장 나중인 수열의 $i$ 번째 수입니다.
문제의 이름을 보고 LCS (Longest Common Subsequence) 알고리즘을 활용하면 되는 문제라고 추측했다.
내가 사용한 방식은 기존의 LCS를 찾는 알고리즘을 이용하여 LCS를 찾은 뒤, 그중 가장 큰 수부터 수열이 시작되도록 자른 뒤 그 수열을 답으로 출력하게 하는 방식이다.
코드는 다음과 같이 작성하였다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
cin >> N;
vector<int> A(N, 0);
for (int i=0; i<N; i++) {
cin >> A[i];
}
int M;
cin >> M;
vector<int> B(M, 0);
for (int i=0; i<M; i++) {
cin >> B[i];
}
vector<vector<int>> dp(N+1, vector<int>(M+1, 0));
for (int i=1; i<=N; i++) {
for (int j=1; j<=M; j++) {
if (A[i-1] == B[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
}
else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
vector<int> result;
if (dp[N][M] != 0) {
int i = N;
int j = M;
while (i > 0 && j > 0) {
if (A[i-1] == B[j-1]) {
result.emplace_back(A[i-1]);
i--;
j--;
}
else {
if (dp[i-1][j] > dp[i][j-1]) {
i--;
}
else {
j--;
}
}
}
reverse(result.begin(), result.end());
int maximum = 0;
int largestIndex = 0;
for (int i=0; i<N; i++) {
if (maximum < result[i]) {
maximum = result[i];
largestIndex = i;
}
}
cout << result.size() - largestIndex << "\n";
for (int i=largestIndex; i<result.size(); i++) {
cout << result[i] << " ";
}
}
else {
cout << 0;
}
return 0;
}
실행 결과 ‘틀렸습니다’ 가 떴다.
이전 제출에서 내가 간과한 부분으로 떠올린 것은, LCS의 부분 수열 중 사전 순으로 가장 앞서는 것이 꼭 연속적으로 나열된 부분 수열일 필요가 없다는 것이었다.
예를 들어 LCS가 ${3,\ 1,\ 2}$ 라고 하면, 이전 제출의 경우 연속적으로 나열된 부분 수열만을 추출하기 때문에 ${3,\ 1}$ 을 얻어내지만, 실제로 사전 순으로 가장 앞서는 것은 ${3,\ 2}$ 로, 제대로 된 결과를 얻어내지 못하는 것이다.
이를 해결하기 위해서 LCS를 구한 뒤 LCS의 부분 수열을 추출하는 과정을 수정하였다. std::stack
을 활용하여 큰 수들을 최대한 앞쪽으로 배치하도록 하여 사전 순으로 가장 앞서는 것을 찾을 수 있게 하였다.
코드는 다음과 같이 수정하였다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
cin >> N;
vector<int> A(N, 0);
for (int i=0; i<N; i++) {
cin >> A[i];
}
int M;
cin >> M;
vector<int> B(M, 0);
for (int i=0; i<M; i++) {
cin >> B[i];
}
vector<vector<int>> dp(N+1, vector<int>(M+1, 0));
for (int i=1; i<=N; i++) {
for (int j=1; j<=M; j++) {
if (A[i-1] == B[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
}
else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
vector<int> lcs;
if (dp[N][M] != 0) {
int i = N;
int j = M;
while (i > 0 && j > 0) {
if (A[i-1] == B[j-1]) {
lcs.emplace_back(A[i-1]);
i--;
j--;
}
else {
if (dp[i-1][j] > dp[i][j-1]) {
i--;
}
else {
j--;
}
}
}
reverse(lcs.begin(), lcs.end());
stack<int> result;
result.push(lcs[0]);
for (int i=1; i<lcs.size(); i++) {
if (result.top() < lcs[i]) {
while (!result.empty() && result.top() < lcs[i]) {
result.pop();
}
}
result.push(lcs[i]);
}
cout << result.size() << "\n";
vector<int> finalResult;
while (!result.empty()) {
finalResult.emplace_back(result.top());
result.pop();
}
reverse(finalResult.begin(), finalResult.end());
for (auto& i : finalResult) {
cout << i << " ";
}
}
else {
cout << 0;
}
return 0;
}
실행 결과 ‘틀렸습니다’ 가 떴다.
LCS를 활용한 2개의 제출이 모두 실패했고, 질문 게시판을 이용하여 반례를 살펴본 결과 애초에 이 문제에서 답을 구할 때 LCS의 부분 수열중에는 답이 없을 수 있다는 결론에 도달하게 되었다.
3번째 제출부터 선택한 새로운 방식은, $A$ 수열과 $B$ 수열의 범위가 1부터 100 사이라는 점을 이용하여 타겟을 100부터 1로 줄여나가면서 각 수열과 일치하는 것이 있는지 확인하고, 있다면 타겟을 정답 수열에 포함시킨 뒤, 각 수열에서 발견된 위치 이후의 인덱스 부터 다시 탐색을 진행하는 방식을 이용하게 하였다.
코드는 다음과 같이 수정하였다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
cin >> N;
vector<int> A(N);
for (int i=0; i<N; i++) {
cin >> A[i];
}
int M;
cin >> M;
vector<int> B(M);
for (int i=0; i<M; i++) {
cin >> B[i];
}
vector<int> result;
int startA = 0;
int startB = 0;
for (int target=100; target>0; target--) {
bool found = false;
for (int i=startA; i<N; i++) {
if (A[i] == target) {
for (int j=startB; j<M; j++) {
if (B[j] == target) {
result.emplace_back(target);
startA = i+1;
startB = j+1;
found = true;
break;
}
}
}
if (found) break;
}
}
cout << result.size() << "\n";
for (auto& i : result) {
cout << i << " ";
}
return 0;
}
실행 결과 ‘틀렸습니다’ 가 떴다.
3번째 제출에서 간과한 부분은 정답 수열에 같은 수가 여러 번 나올 수 있다는 것이었다. 지금과 같은 방식으로는 100부터 1까지 정답 수열에 한 번씩만 나오게 할 수 있는데, 이 때문에 실제 정답과 차이가 발생할 수 있는 것이었다.
이를 해결하기 위해 사용한 방식은 기존의 3중 반복문에 반복문을 하나 더 씌워 중복된 수도 검출해낼 수 있게 하는 것이었다.
코드는 다음과 같이 수정하였다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
cin >> N;
vector<int> A(N);
for (int i=0; i<N; i++) {
cin >> A[i];
}
int M;
cin >> M;
vector<int> B(M);
for (int i=0; i<M; i++) {
cin >> B[i];
}
vector<int> result;
int startA = 0;
int startB = 0;
while (startA < N && startB < M) {
bool found = false;
for (int target=100; target>0; target--) {
for (int i=startA; i<N; i++) {
if (A[i] == target) {
for (int j=startB; j<M; j++) {
if (B[j] == target) {
result.emplace_back(target);
startA = i+1;
startB = j+1;
found = true;
break;
}
}
}
if (found) break;
}
if (found) break;
}
}
cout << result.size() << "\n";
for (auto& i : result) {
cout << i << " ";
}
return 0;
}
실행 결과 ‘시간 초과’가 떴다.
4번째 시도에서 시간 초과가 발생한 것은 시간 복잡도가 너무 커져버렸기 때문이라고 생각했다.
그래서 3번째 제출때의 코드를 이번에는 중간에 같은 수가 발견되었다고해서 바로 빠져나오지 않고 계속 타겟값을 유지시킨 뒤 탐색을 진행하도록 해보았다.
코드는 다음과 같이 수정하였다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
cin >> N;
vector<int> A(N);
for (int i=0; i<N; i++) {
cin >> A[i];
}
int M;
cin >> M;
vector<int> B(M);
for (int i=0; i<M; i++) {
cin >> B[i];
}
vector<int> result;
int startA = 0;
int startB = 0;
for (int target=100; target>0; target--) {
for (int i=startA; i<N; i++) {
if (A[i] == target) {
for (int j=startB; j<M; j++) {
if (B[j] == target) {
result.emplace_back(target);
startA = i+1;
startB = j+1;
break;
}
}
}
}
}
cout << result.size() << "\n";
for (auto& i : result) {
cout << i << " ";
}
return 0;
}
그러자 모든 테스트 케이스를 통과하고 ‘맞았습니다’가 나오는 것을 확인할 수 있었다.
오랫동안 CLASS 4 문제를 풀지 않았기 때문에 CLASS 4 만의 매운맛을 잊고 있었다! 애초에 기존에 알던 LCS 알고리즘을 무조건 이용해야 한다고 착각했던 것이 가장 큰 문제점이었던 것 같다…
어쨌거나 솔브드 업데이트로 인해 뺏겼던 CLASS 4 금장을 다시 되찾는데 성공하였다!
이제 남은 건 CLASS 5 은장과 금장이다. 예전의 영광을 되찾기 위해서는 열심히 노력해야 할 것 같다…
오늘의 PS는 여기까지!
1: https://www.acmicpc.net/problem/30805